 package com.storage.framework.shiro.web.filter.jwt;

 import com.auth0.jwt.interfaces.DecodedJWT;
 import com.google.gson.JsonObject;
 import com.storage.framework.jwt.Constant;
 import com.storage.framework.jwt.GlobalError;
 import com.storage.framework.jwt.JwtToken;
 import com.storage.framework.shiro.web.filter.LogoutFilter;
 import com.storage.framework.util.JwtUtil;
 import com.storage.system.service.IRedisService;
 import org.apache.shiro.authc.IncorrectCredentialsException;
 import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.RequestMethod;

 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.Objects;

 /**
 * @author lixiao
 * @date 2019/8/5 23:46
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

    private static final Logger log = LoggerFactory.getLogger(LogoutFilter.class);

    private IRedisService redisService;

    public JwtFilter() {
    }

    public JwtFilter(IRedisService redisService) {
        this.redisService = redisService;
    }

    /**
     * 执行登录认证
     * JWTToken刷新生命周期
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
     * 2、当该用户在次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWTToken值还在生命周期内，则会通过重新PUT的方式k、v都为Token值，缓存中的token值生命周期时间重新计算(这时候k、v值一样)
     * 4、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，
     *    程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 5、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 6、每次当返回为true情况下，都会给Response的Header中设置Authorization，该Authorization映射的v为cache对应的v值。
     * 7、注：当前端接收到Response的Header中的Authorization值会存储起来，作为以后请求token使用
     * @param request ServletRequest
     * @param response ServletResponse
     * @param mappedValue mappedValue
     * @return 是否成功
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        String token = ((HttpServletRequest) request).getHeader(Constant.TOKEN_HEADER_NAME);
        String type = ((HttpServletRequest) request).getHeader(Constant.TYPE_HEADER_NAME);

        if (token == null || type == null) {
            writer(response, GlobalError.MISTAKE_REQUIRED_TYPE_AND_TOKEN, null);
            return false;
        } else if (!Objects.equals(type, JwtUtil.getType(token))) {
            writer(response, GlobalError.TYPE_NOT_MATCH, null);
        }
        JwtToken jwtToken = new JwtToken(token);
        try {
            // 提交给realm进行登入，验证是否过期，如果过期就会抛出异常
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (IncorrectCredentialsException ex) {
            if (Objects.equals(GlobalError.PASSWORD_HAS_CHANGED.getCode(), ex.getMessage())) {
                writer(response, GlobalError.PASSWORD_HAS_CHANGED, null);
            }

            DecodedJWT decodedJWT = JwtUtil.getDecodedJWT(token);
            // token 不在生命周期内(1.超过规定的最大免登陆周期1个月;2-为超过规定的最大免登陆周期)
            Long diff = System.currentTimeMillis() - decodedJWT.getExpiresAt().getTime();
            if (diff > Constant.MAX_EXPIRE_TIME) {
                writer(response, GlobalError.TOKEN_OVER_MAX_EXPIRE_TIME, null);
                return false;
            } else {
                // 重新生成token并放入redis中返回前端替换老token发送请求
                token = decodedJWT.getToken();
                synchronized (this) {
                    if (!redisService.hasKey(token)) {
                        //设置老token 30秒的存活期
                        redisService.setKey(token, JwtUtil.getPhone(token),30 * 1000L);
                        // 生成新token
                        String newToken = JwtUtil.sign(JwtUtil.getPhone(token), Long.parseLong(JwtUtil.getUserId(token)), JwtUtil.getSecret(token), type);
                        redisService.setKey(newToken, newToken, Constant.TOKEN_EXPIRE_TIME);
                        writer(response, GlobalError.TOKEN_HAS_EXPIRED, newToken);
                    } else {
                        // 缓存中有就直接放过请求
                        return true;
                    }
                }
            }
        }
        return false;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

     /**
      * filter 过滤写出对应信息
      * @param response
      * @param globalError
      */
    private void writer(ServletResponse response, GlobalError globalError, String token) {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            JsonObject message = new JsonObject();
            message.addProperty("code", globalError.getCode());
            message.addProperty("msg", globalError.getErrMsg());
            message.addProperty("data", token);
            response.getWriter().print(message.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
